Skip to content

Feature: add a new event for many-to-many relationships#498

Open
andrestejerina97 wants to merge 4 commits intomainfrom
feature/new-event-for-formatters
Open

Feature: add a new event for many-to-many relationships#498
andrestejerina97 wants to merge 4 commits intomainfrom
feature/new-event-for-formatters

Conversation

@andrestejerina97
Copy link
Copy Markdown
Contributor

@andrestejerina97 andrestejerina97 commented Feb 11, 2026

ref: https://app.clickup.com/t/86b84w9x2

Summary by CodeRabbit

  • New Features
    • Audit logs now track many-to-many relationship changes with detailed information about which entities were added or removed.
    • PresentationCategoryGroup entity modifications are now audited.
    • Relationship changes display specific entity IDs for improved traceability and visibility.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 2 times, most recently from ffb0da8 to e142976 Compare February 13, 2026 19:22
@andrestejerina97 andrestejerina97 marked this pull request as ready for review February 18, 2026 14:02
@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 3 times, most recently from 1613295 to bd0d027 Compare February 18, 2026 18:02
@andrestejerina97
Copy link
Copy Markdown
Contributor Author

@martinquiroga-exo ready to review

Copy link
Copy Markdown
Contributor

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

];

$eventType = $isManyToMany
? ($isDeletion ? IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE : IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this concatenated ternary operators, perhaps I would like to see an exit early guard clause when the event is an EVENT_COLLECTION_UPDATE.

I defer to Casey on this one since is a design issue rather than an implementation one.

Copy link
Copy Markdown
Contributor

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to defer to whatever Seb comes up with but this is what I'm seeing.

The auditCollection() method changed the calling convention for all collection updates not just ManyToMany. For non-ManyToMany collections, it now passes $owner (the entity) as the subject instead of $col (the collection itself), and $payload instead of [] as the change set.

Both strategies downstream expect a PersistentCollection as $subject for EVENT_COLLECTION_UPDATE:

  • OTLP strategy: instanceof PersistentCollection checks fail, formatter returns null, audit entries silently dropped
  • DB strategy: calls count($subject) and $subject->getSnapshot() on the entity — throws \Error (not \Exception), uncaught by the catch block

Fix: Unless the intent was to actually change the behavior for all for some reason, only change the subject/payload for ManyToMany events; preserve the original $strategy->audit($col, [], EVENT_COLLECTION_UPDATE, $ctx) call for non-ManyToMany collections.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 3 times, most recently from 090b9c5 to 186913f Compare February 26, 2026 19:06
@andrestejerina97
Copy link
Copy Markdown
Contributor Author

@caseylocker it's ready to review

Copy link
Copy Markdown
Contributor

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two fixes needed before merge:

  1. app/Audit/AuditLogOtlpStrategy.php — Add M2M event mappings

mapEventTypeToAction() and getLogMessage() need explicit cases for EVENT_COLLECTION_MANYTOMANY_UPDATE (→ ACTION_COLLECTION_UPDATE / LOG_MESSAGE_COLLECTION_UPDATED) and EVENT_COLLECTION_MANYTOMANY_DELETE (→ ACTION_DELETE / LOG_MESSAGE_DELETED). Without this, M2M audit entries hit Elasticsearch with action=unknown, making them invisible to dashboards/queries filtering on action type.

buildAuditLogData() should also extract collection metadata from $change_set['collection'] for M2M events, since the subject is the owner entity (not a PersistentCollection).

  1. app/Audit/AuditLogFormatterFactory.php — Fix generic M2M fallback

Lines 110-123: when no context-specific formatter exists, the fallback creates EntityCollectionUpdateAuditLogFormatter and passes the owner entity as $subject. That formatter calls $subject->getInsertDiff() (line 50 of EntityCollectionUpdateAuditLogFormatter), which doesn't exist on the owner entity. This either silently drops the audit entry or throws an uncaught \Error. The fallback should pass $change_set['collection'] as the subject instead of the owner, or use a M2M-aware generic formatter.

Why this blocks merge: Without fix 1, OTLP audit data is misclassified. Without fix 2, any M2M collection change on an entity without a registered custom formatter will silently fail or crash.

@matiasperrone-exo matiasperrone-exo force-pushed the feature/new-event-for-formatters branch from 186913f to 8af3df1 Compare March 3, 2026 18:54
@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 2 times, most recently from 02a1841 to be8a5f1 Compare March 5, 2026 17:21
@smarcet
Copy link
Copy Markdown
Collaborator

smarcet commented Mar 5, 2026

@andrestejerina97 please review unit tests https://github.com/OpenStackweb/summit-api/actions/runs/22728527050/job/65910842285?pr=498#step:5:68

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 2 times, most recently from 6b78204 to 9d27bf5 Compare March 5, 2026 18:55
@andrestejerina97
Copy link
Copy Markdown
Contributor Author

@caseylocker it's ready to review

@smarcet smarcet force-pushed the feature/new-event-for-formatters branch from 1d0e92b to b3d0359 Compare March 6, 2026 13:02
@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from b3d0359 to 1e069fd Compare March 11, 2026 14:20
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces comprehensive many-to-many collection auditing support to the audit log system. New constants, metadata models, and event handlers enable detection and formatting of changes to many-to-many relationships, with support for both initialized and uninitialized collections via join table queries.

Changes

Cohort / File(s) Summary
Core Audit Interface Extension
app/Audit/Interfaces/IAuditStrategy.php
Added four new public constants for many-to-many collection events: EVENT_COLLECTION_MANYTOMANY_UPDATE, EVENT_COLLECTION_MANYTOMANY_DELETE, and corresponding action types.
Metadata & Helper Classes
app/Audit/PersistentCollectionMetadata.php
New class modeling collection metadata with factory method to extract mapping, initialization state, and optional preloaded deleted IDs from Doctrine PersistentCollection instances.
Abstract Formatter Enhancements
app/Audit/AbstractAuditLogFormatter.php
Added four protected helper methods: handleManyToManyCollection() for metadata extraction, processCollection() for computing added/removed IDs, extractCollectionEntityIds() for ID normalization, and formatManyToManyDetailedMessage() for message rendering.
Listener Collection Auditing
app/Audit/AuditEventListener.php
Extended to audit many-to-many collections via new private methods: auditCollection() gates auditing with conditional payload enrichment, and fetchManyToManyIds() queries join tables for uninitialized collections. Stores entity manager in instance property.
Formatter Factory & Strategy
app/Audit/AuditLogFormatterFactory.php, app/Audit/AuditLogOtlpStrategy.php
Factory routes many-to-many events to context-specific or default formatters. Strategy extended to compute collection counts/diffs conditionally based on initialization state and to map new event types to actions and log messages.
Default Formatters
app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php, app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
New formatter implementations handling many-to-many update/delete events with optional child-entity formatting and detailed message generation with added/removed ID lists.
Concrete Entity Formatters
app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php, app/Audit/ConcreteFormatters/PresentationCategoryGroupAuditLogFormatter.php
Extended to route many-to-many events to specialized handlers, with custom formatting for audit context and ID list rendering. PresentationCategoryGroup also updated entity configuration in config/audit_log.php.
Formatter Tests (Core Behavior)
tests/OpenTelemetry/Formatters/AuditEventListenerTest.php, tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php, tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php
Added comprehensive test suites for listener private methods, default formatter logic, child formatter integration, and edge cases (empty diffs, uninitialized collections, exception handling).
Entity-Specific Formatter Tests
tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php, tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php
Added test suites validating entity-specific formatter handling of many-to-many events with various payload and diff scenarios.
Integration & Support
tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php, tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
Updated integration tests to include new event types in formatter discovery. Added test helper to construct mocked PersistentCollection instances for testing without a real database.

Sequence Diagram(s)

sequenceDiagram
    participant Listener as AuditEventListener
    participant EventArgs as OnFlushEventArgs
    participant EM as EntityManager
    participant UoW as UnitOfWork
    participant Formatter as AuditLogFormatter
    participant DB as Database (DBAL)
    participant Strategy as AuditLogOtlpStrategy

    Listener->>EventArgs: onFlush()
    EventArgs->>Listener: provide EM
    Listener->>EM: getUnitOfWork()
    EM->>UoW: return UoW
    UoW->>Listener: scheduled collections

    Listener->>Listener: auditCollection(subject, eventType)
    alt Many-to-Many Collection
        Listener->>Listener: check owning side & owner exists
        alt Uninitialized or Empty Diff
            Listener->>DB: fetchManyToManyIds()
            DB->>DB: query join table
            DB->>Listener: return integer IDs
            Listener->>Listener: build deleted_ids payload
        end
        Listener->>Listener: return (owner_entity, payload, event_type)
    else Non-Many-to-Many
        Listener->>Listener: return (collection, {}, EVENT_COLLECTION_UPDATE)
    end

    Listener->>Formatter: format(subject, change_set)
    Formatter->>Formatter: handleManyToManyCollection(change_set)
    Formatter->>Formatter: processCollection(metadata)
    Formatter->>Formatter: computeAddedRemovedIds()
    Formatter->>Formatter: formatManyToManyDetailedMessage()
    Formatter->>Listener: return formatted string

    Listener->>Strategy: map event type to action
    Strategy->>Strategy: compute collection attributes
    Strategy->>Listener: return enriched audit context
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 The many-to-many maze, now audited with care,
Deleted IDs tracked through the join-table air,
Collections uninitialized? We query the store,
Each added, each removed—forever logged and more! 🌲

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature: add a new event for many-to-many relationships' accurately and concisely describes the main change: extending the audit system to support a new event type for many-to-many collection updates and deletions, which aligns with the extensive changes across multiple audit-related files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/new-event-for-formatters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@andrestejerina97
Copy link
Copy Markdown
Contributor Author

@martinquiroga-exo ready to review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php (1)

147-169: ⚠️ Potential issue | 🟠 Major

Fail on unsupported event types.

$unsupported is populated but never asserted. Any formatter that still rejects EVENT_COLLECTION_MANYTOMANY_UPDATE or EVENT_COLLECTION_MANYTOMANY_DELETE through the "event type" path will keep this test green, so the new coverage can miss the PR's main behavior.

🛠️ Proposed fix
-        $this->assertEmpty($errors, "Event type handling failed:\n" . implode("\n", $errors));
+        $this->assertEmpty($unsupported, "Unsupported event types:\n" . implode("\n", $unsupported));
+        $this->assertEmpty($errors, "Event type handling failed:\n" . implode("\n", $errors));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php` around lines
147 - 169, The test collects unsupported formatters in $unsupported but never
fails on them, allowing regressions; after the loop and before asserting
$errors, add an assertion that $unsupported is empty (with a clear message) so
any formatterClass from discoverFormatters() that rejects event types like
EVENT_COLLECTION_MANYTOMANY_UPDATE or EVENT_COLLECTION_MANYTOMANY_DELETE (caught
via the "event type" path in the catch block) will fail the test; locate the
loop using FormatterTestHelper::assertFormatterCanBeInstantiated and the
$unsupported array and assertEmpty($unsupported, "Unsupported event types
found:\n" . implode("\n", $unsupported)).
🧹 Nitpick comments (7)
app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php (2)

118-123: Remove extraneous blank lines.

Lines 119-121 contain unnecessary whitespace that appears to be a leftover from editing.

🧹 Proposed fix
         try {
             $removed_ids = $collectionData['removed_ids'] ?? [];
-
-            
-
             $field = $collectionData['field'] ?? 'unknown';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php` around
lines 118 - 123, Remove the extraneous blank lines between the assignment of
$removed_ids and the subsequent assignments to $field and $targetEntity in the
SummitAttendeeAuditLogFormatter class so the code reads compactly as consecutive
assignments (ensure $removed_ids = $collectionData['removed_ids'] ?? []; is
immediately followed by $field = $collectionData['field'] ?? 'unknown'; and
$targetEntity = $collectionData['target_entity'] ?? 'unknown';).

59-76: Remove unused $subject parameter.

The $subject parameter is declared but never used within handleManyToManyCollection. Since $id and $name are already extracted and passed separately, this parameter is redundant.

♻️ Proposed fix
-    private function handleManyToManyCollection(SummitAttendee $subject, array $change_set, $id, $name, bool $isDeletion): ?string
+    private function handleManyToManyCollection(array $change_set, $id, $name, bool $isDeletion): ?string
     {

Also update the call sites in lines 47 and 50:

                 case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE:
-                    return $this->handleManyToManyCollection($subject, $change_set, $id, $name, false);
+                    return $this->handleManyToManyCollection($change_set, $id, $name, false);

                 case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE:
-                    return $this->handleManyToManyCollection($subject, $change_set, $id, $name, true);
+                    return $this->handleManyToManyCollection($change_set, $id, $name, true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php` around
lines 59 - 76, The method handleManyToManyCollection has an unused $subject
parameter; remove $subject from its signature (change function
handleManyToManyCollection(SummitAttendee $subject, array $change_set, $id,
$name, bool $isDeletion): ?string to handleManyToManyCollection(array
$change_set, $id, $name, bool $isDeletion): ?string) and update all call sites
that pass the subject (the two places invoking handleManyToManyCollection) to
stop providing the extra argument so they pass only $change_set, $id, $name,
$isDeletion; ensure type hints and usages inside handleManyToManyCollection
remain consistent after removal.
app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php (1)

55-56: Consider: Inconsistent key naming between added_ids/removed_ids and deleted_ids.

The code checks for both 'removed_ids' and 'deleted_ids' as fallback. While this provides flexibility, it may lead to confusion about which key should be used. Consider documenting the expected payload structure or standardizing on one key name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php`
around lines 55 - 56, Standardize the change_set keys by treating "removed_ids"
as the canonical name and explicitly map any legacy "deleted_ids" to it before
use in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter: in the code
that prepares $change_set (or immediately before computing
$addedIds/$removedIds) normalize $change_set['removed_ids'] =
$change_set['removed_ids'] ?? $change_set['deleted_ids'] ?? null, then set
$addedIds = $change_set['added_ids'] ?? null and $removedIds =
$change_set['removed_ids']; also update any docblocks/comments or
function/method docs that reference $change_set to state that "removed_ids" is
the standard key (and "deleted_ids" is supported as an alias).
app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php (1)

60-65: Debug logging may be verbose in production.

The debug logging includes the full deletedIds array content. For collections with many IDs, this could produce very large log entries. Consider logging only the count in debug mode.

🔧 Suggested improvement
             $deletedIds = isset($change_set['deleted_ids']) ? $change_set['deleted_ids'] : null;
             Log::debug("DefaultEntityManyToManyCollectionDeleteAuditLogFormatter::format - deletedIds", [
-                'deletedIds' => $deletedIds,
                 'isNull' => $deletedIds === null,
                 'count' => $deletedIds ? count($deletedIds) : 0
             ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php`
around lines 60 - 65, The debug statement in
DefaultEntityManyToManyCollectionDeleteAuditLogFormatter::format currently logs
the entire $deletedIds array which can be huge; change the Log::debug call to
avoid serializing the full array and instead log only the count (use
count($deletedIds) or 0 when null) and a short flag indicating null/empty,
referencing the $deletedIds variable so the formatter still provides useful
diagnostic info without dumping the full collection.
app/Audit/AbstractAuditLogFormatter.php (1)

189-239: Edge case: Empty message detail when no IDs present.

When both $addedIds and $removedIds are empty arrays, formatManyToManyDetailedMessage will produce a message ending with a colon and empty detail: "Many-to-Many collection 'field' action: ". Consider handling this edge case.

🔧 Suggested improvement
     protected static function formatManyToManyDetailedMessage(array $details, int $addCount, int $removeCount, string $action): string
     {
         $field = $details['field'] ?? 'unknown';
         $target = $details['target_entity'] ?? 'unknown';
         $addedIds = $details['added_ids'] ?? [];
         $removedIds = $details['removed_ids'] ?? [];

         $parts = [];
         if (!empty($addedIds)) {
             $parts[] = sprintf("Added %d %s(s): %s", $addCount, $target, implode(', ', $addedIds));
         }
         if (!empty($removedIds)) {
             $parts[] = sprintf("Removed %d %s(s): %s", $removeCount, $target, implode(', ', $removedIds));
         }

         $detailStr = implode(' | ', $parts);
+        if (empty($detailStr)) {
+            return sprintf("Many-to-Many collection '%s' %s: no changes", $field, $action);
+        }
         return sprintf("Many-to-Many collection '%s' %s: %s", $field, $action, $detailStr);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AbstractAuditLogFormatter.php` around lines 189 - 239,
formatManyToManyDetailedMessage can produce a trailing colon with empty details
when both added and removed ID lists are empty; change the method to detect when
$parts is empty and return a cleaner message (e.g., "Many-to-Many collection
'FIELD' ACTION: no changes" or "Many-to-Many collection 'FIELD' ACTION" without
the colon). Update the logic in formatManyToManyDetailedMessage to check if
$parts is empty and set $detailStr to a sensible placeholder (or omit the colon)
before building the final sprintf; reference the method name
formatManyToManyDetailedMessage and the keys 'field', 'added_ids', 'removed_ids'
to locate the code to modify. Ensure unit/output formatting remains consistent
with existing messages.
app/Audit/AuditLogOtlpStrategy.php (1)

200-209: Minor: Inconsistent error message format.

The error message at line 209 has an unusual format with "AuditLogOtlpStrategy:: unknown targetEntity" (double colon followed by space). Consider using a consistent format.

🔧 Suggested fix
         } catch (\Exception $ex) {
-            return 'AuditLogOtlpStrategy:: unknown targetEntity';
+            return 'unknown';
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditLogOtlpStrategy.php` around lines 200 - 209, The catch block
in AuditLogOtlpStrategy that currently returns "AuditLogOtlpStrategy:: unknown
targetEntity" should be changed to a consistently formatted message (for example
"AuditLogOtlpStrategy: unknown targetEntity") and preferably include the
exception details; update the return in the catch of the method that calls
$collection->getMapping() to return a string like "AuditLogOtlpStrategy: unknown
targetEntity - " . $ex->getMessage() (or at minimum remove the double colon and
extra space so it reads "AuditLogOtlpStrategy: unknown targetEntity").
tests/OpenTelemetry/Formatters/AuditEventListenerTest.php (1)

66-122: Add listener coverage for initialized collection deletions.

AuditEventListener::auditCollection() now has a separate delete path for initialized many-to-many collections via fetchDeletedIdsFromInitializedCollection(), but this file only exercises the non-many-to-many branch and the raw ID fetch helper. That leaves the new diff-based deleted_ids payload untested at the listener level. PersistentCollectionTestHelper should make this path straightforward to cover.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/OpenTelemetry/Formatters/AuditEventListenerTest.php` around lines 66 -
122, Add a new unit test that exercises AuditEventListener::auditCollection()'s
many-to-many delete branch by constructing an initialized PersistentCollection
(use PersistentCollectionTestHelper to create a collection that is initialized
and has removed elements), attach it to a mock owner and mapping for a
ManyToManyOwningSideMapping, inject a mocked EntityManager (and ClassMetadata)
into the AuditEventListener instance, then call auditCollection() and assert the
emitted payload includes the diff-style "deleted_ids" (e.g., [10,11]) produced
by AuditEventListener::fetchDeletedIdsFromInitializedCollection(); ensure the
test uses the same mocking pattern as existing tests (QueryBuilder/Connection
mocks if needed) and targets the listener-level behavior rather than the helper
method directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Audit/AuditEventListener.php`:
- Around line 216-285: Remove the unused $uow parameter from the
fetchManyToManyIds method signature and any related docblock, then update all
call sites that pass $uow into fetchManyToManyIds to call it without that
argument; specifically edit the AuditEventListener::fetchManyToManyIds
declaration to drop $uow and modify the internal callers within the class to
call fetchManyToManyIds($collection) instead of passing the unused $uow. Ensure
nothing else relies on the removed parameter and run static analysis/tests to
confirm no remaining references.
- Line 114: The $ui array is initialized but never populated, making later
accesses to $ui['app'] and $ui['flow'] always null; update the
AuditEventListener code to either remove the unused $ui initialization and use
null/defaults directly where $ui['app']/$ui['flow'] are read, or populate $ui
from the current request/route context (e.g. pull values from the Request or
route attributes) before those reads so $ui['app'] and $ui['flow'] contain the
intended values; ensure you update the same method in AuditEventListener where
$ui is declared and where $ui['app']/$ui['flow'] are accessed.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php`:
- Around line 96-109: The child-entity creation loop is pushing potentially null
returns from $this->child_entity_formatter->format(...) into $changes, causing
null entries in the final string; update the creation loop (the foreach over
$insertDiff calling format with
IChildEntityAuditLogFormatter::CHILD_ENTITY_CREATION) to only append non-null
results (e.g., check the formatter return is not null before $changes[] = ...)
or run an array_filter on $changes before the implode, mirroring the
null-filtering behavior used for deletions (format(..., CHILD_ENTITY_DELETION)).

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php`:
- Around line 109-112: The formatters in SummitAttendeeAuditLogFormatter are
inconsistent: formatManyToManyUpdate catches exceptions and returns a fallback
message, while formatManyToManyDelete returns null; change
formatManyToManyDelete to mirror formatManyToManyUpdate by catching \Throwable
and returning the same formatted fallback string (e.g., using $id, $name and
$this->getUserInfo()) and logging the exception with Log::warning so both update
and delete produce a consistent audit fallback on error.

In
`@tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php`:
- Around line 250-264: The current formatter preserves an empty segment when a
child formatter returns null, causing a leading '|' in
DefaultEntityManyToManyCollectionUpdateAuditLogFormatter output; update the
format method in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter so it
collects child outputs from the IChildEntityAuditLogFormatter->format calls,
filters out null/empty values, and then joins the remaining pieces with the '|'
separator (only inserting separators between non-empty segments) so null child
results are skipped and no leading separator is emitted.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`:
- Around line 70-79: The test ManyToMany null-case provider currently always
calls format($attendee, []) so handleManyToManyCollection() exits on missing
collection and the DP_*_WITHOUT_CONTEXT rows never exercise the "collection
present but context missing" branch; update the DataProvider
'providesNullCasesForManyToMany' rows labeled DP_*_WITHOUT_CONTEXT to pass an
input array that includes a valid collection key (e.g. ['collection' => <dummy
collection value>]) but omits the required context key so
testManyToManyReturnsNullWithoutRequiredContextOrCollection (and the duplicate
cases around lines 203-210) will hit the no-context path in
handleManyToManyCollection().

---

Outside diff comments:
In `@tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php`:
- Around line 147-169: The test collects unsupported formatters in $unsupported
but never fails on them, allowing regressions; after the loop and before
asserting $errors, add an assertion that $unsupported is empty (with a clear
message) so any formatterClass from discoverFormatters() that rejects event
types like EVENT_COLLECTION_MANYTOMANY_UPDATE or
EVENT_COLLECTION_MANYTOMANY_DELETE (caught via the "event type" path in the
catch block) will fail the test; locate the loop using
FormatterTestHelper::assertFormatterCanBeInstantiated and the $unsupported array
and assertEmpty($unsupported, "Unsupported event types found:\n" . implode("\n",
$unsupported)).

---

Nitpick comments:
In `@app/Audit/AbstractAuditLogFormatter.php`:
- Around line 189-239: formatManyToManyDetailedMessage can produce a trailing
colon with empty details when both added and removed ID lists are empty; change
the method to detect when $parts is empty and return a cleaner message (e.g.,
"Many-to-Many collection 'FIELD' ACTION: no changes" or "Many-to-Many collection
'FIELD' ACTION" without the colon). Update the logic in
formatManyToManyDetailedMessage to check if $parts is empty and set $detailStr
to a sensible placeholder (or omit the colon) before building the final sprintf;
reference the method name formatManyToManyDetailedMessage and the keys 'field',
'added_ids', 'removed_ids' to locate the code to modify. Ensure unit/output
formatting remains consistent with existing messages.

In `@app/Audit/AuditLogOtlpStrategy.php`:
- Around line 200-209: The catch block in AuditLogOtlpStrategy that currently
returns "AuditLogOtlpStrategy:: unknown targetEntity" should be changed to a
consistently formatted message (for example "AuditLogOtlpStrategy: unknown
targetEntity") and preferably include the exception details; update the return
in the catch of the method that calls $collection->getMapping() to return a
string like "AuditLogOtlpStrategy: unknown targetEntity - " . $ex->getMessage()
(or at minimum remove the double colon and extra space so it reads
"AuditLogOtlpStrategy: unknown targetEntity").

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php`:
- Around line 60-65: The debug statement in
DefaultEntityManyToManyCollectionDeleteAuditLogFormatter::format currently logs
the entire $deletedIds array which can be huge; change the Log::debug call to
avoid serializing the full array and instead log only the count (use
count($deletedIds) or 0 when null) and a short flag indicating null/empty,
referencing the $deletedIds variable so the formatter still provides useful
diagnostic info without dumping the full collection.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php`:
- Around line 55-56: Standardize the change_set keys by treating "removed_ids"
as the canonical name and explicitly map any legacy "deleted_ids" to it before
use in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter: in the code
that prepares $change_set (or immediately before computing
$addedIds/$removedIds) normalize $change_set['removed_ids'] =
$change_set['removed_ids'] ?? $change_set['deleted_ids'] ?? null, then set
$addedIds = $change_set['added_ids'] ?? null and $removedIds =
$change_set['removed_ids']; also update any docblocks/comments or
function/method docs that reference $change_set to state that "removed_ids" is
the standard key (and "deleted_ids" is supported as an alias).

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php`:
- Around line 118-123: Remove the extraneous blank lines between the assignment
of $removed_ids and the subsequent assignments to $field and $targetEntity in
the SummitAttendeeAuditLogFormatter class so the code reads compactly as
consecutive assignments (ensure $removed_ids = $collectionData['removed_ids'] ??
[]; is immediately followed by $field = $collectionData['field'] ?? 'unknown';
and $targetEntity = $collectionData['target_entity'] ?? 'unknown';).
- Around line 59-76: The method handleManyToManyCollection has an unused
$subject parameter; remove $subject from its signature (change function
handleManyToManyCollection(SummitAttendee $subject, array $change_set, $id,
$name, bool $isDeletion): ?string to handleManyToManyCollection(array
$change_set, $id, $name, bool $isDeletion): ?string) and update all call sites
that pass the subject (the two places invoking handleManyToManyCollection) to
stop providing the extra argument so they pass only $change_set, $id, $name,
$isDeletion; ensure type hints and usages inside handleManyToManyCollection
remain consistent after removal.

In `@tests/OpenTelemetry/Formatters/AuditEventListenerTest.php`:
- Around line 66-122: Add a new unit test that exercises
AuditEventListener::auditCollection()'s many-to-many delete branch by
constructing an initialized PersistentCollection (use
PersistentCollectionTestHelper to create a collection that is initialized and
has removed elements), attach it to a mock owner and mapping for a
ManyToManyOwningSideMapping, inject a mocked EntityManager (and ClassMetadata)
into the AuditEventListener instance, then call auditCollection() and assert the
emitted payload includes the diff-style "deleted_ids" (e.g., [10,11]) produced
by AuditEventListener::fetchDeletedIdsFromInitializedCollection(); ensure the
test uses the same mocking pattern as existing tests (QueryBuilder/Connection
mocks if needed) and targets the listener-level behavior rather than the helper
method directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 866ce53b-deac-415c-b5d9-86b32cd300b4

📥 Commits

Reviewing files that changed from the base of the PR and between 257cc38 and 1e069fd.

📒 Files selected for processing (15)
  • app/Audit/AbstractAuditLogFormatter.php
  • app/Audit/AuditEventListener.php
  • app/Audit/AuditLogFormatterFactory.php
  • app/Audit/AuditLogOtlpStrategy.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php
  • app/Audit/Interfaces/IAuditStrategy.php
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
  • tests/SummitRSVPServiceTest.php

Comment on lines +250 to +264
public function testFormatWithChildFormatterSkipsNullMessages(): void
{
$childFormatter = Mockery::mock(IChildEntityAuditLogFormatter::class);
$childFormatter
->shouldReceive('format')
->twice()
->andReturn(null, 'child-delete');

$formatterWithChild = new DefaultEntityManyToManyCollectionUpdateAuditLogFormatter($childFormatter);
$formatterWithChild->setContext(AuditContextBuilder::default()->build());

$collection = $this->makeCollection(self::SNAPSHOT_IDS_UPDATE, self::CURRENT_IDS_UPDATE);
$result = $formatterWithChild->format(new \stdClass(), ['collection' => $collection]);

$this->assertSame('|child-delete', $result);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't lock in the leading separator bug.

This expectation codifies the current |child-delete artifact when one child formatter returns null. The delete formatter tests already treat null child output as skippable, so the update contract should do the same instead of preserving an empty segment.

🛠️ Proposed fix
-        $this->assertSame('|child-delete', $result);
+        $this->assertSame('child-delete', $result);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function testFormatWithChildFormatterSkipsNullMessages(): void
{
$childFormatter = Mockery::mock(IChildEntityAuditLogFormatter::class);
$childFormatter
->shouldReceive('format')
->twice()
->andReturn(null, 'child-delete');
$formatterWithChild = new DefaultEntityManyToManyCollectionUpdateAuditLogFormatter($childFormatter);
$formatterWithChild->setContext(AuditContextBuilder::default()->build());
$collection = $this->makeCollection(self::SNAPSHOT_IDS_UPDATE, self::CURRENT_IDS_UPDATE);
$result = $formatterWithChild->format(new \stdClass(), ['collection' => $collection]);
$this->assertSame('|child-delete', $result);
public function testFormatWithChildFormatterSkipsNullMessages(): void
{
$childFormatter = Mockery::mock(IChildEntityAuditLogFormatter::class);
$childFormatter
->shouldReceive('format')
->twice()
->andReturn(null, 'child-delete');
$formatterWithChild = new DefaultEntityManyToManyCollectionUpdateAuditLogFormatter($childFormatter);
$formatterWithChild->setContext(AuditContextBuilder::default()->build());
$collection = $this->makeCollection(self::SNAPSHOT_IDS_UPDATE, self::CURRENT_IDS_UPDATE);
$result = $formatterWithChild->format(new \stdClass(), ['collection' => $collection]);
$this->assertSame('child-delete', $result);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php`
around lines 250 - 264, The current formatter preserves an empty segment when a
child formatter returns null, causing a leading '|' in
DefaultEntityManyToManyCollectionUpdateAuditLogFormatter output; update the
format method in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter so it
collects child outputs from the IChildEntityAuditLogFormatter->format calls,
filters out null/empty values, and then joins the remaining pieces with the '|'
separator (only inserting separators between non-empty segments) so null child
results are skipped and no leading separator is emitted.

Copy link
Copy Markdown
Contributor

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current changes does not adhere to the proposed ADR.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from 1e069fd to 0d6d6aa Compare March 17, 2026 18:41
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/Audit/AbstractAuditLogFormatter.php (1)

102-104: Minor indentation inconsistency.

Line 103 has extra indentation that doesn't match the surrounding code style.

🧹 Suggested fix
     protected function getUserInfo(): string
     {
         if (app()->runningInConsole()) {
-                return 'Worker Job';
+            return 'Worker Job';
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AbstractAuditLogFormatter.php` around lines 102 - 104, In
AbstractAuditLogFormatter (the method containing the app()->runningInConsole()
check) there is an indentation mismatch: the line with "return 'Worker Job';" is
over-indented; adjust the indentation to match the surrounding block style so
the return aligns with the if block's opening brace and other statements in the
method.
app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php (1)

114-117: Remove extraneous blank lines.

Lines 115-117 contain unnecessary whitespace that should be cleaned up for consistency.

🧹 Suggested fix
         try {
             $removed_ids = $collectionData['removed_ids'] ?? [];
-
-            
-
             $field = $collectionData['field'] ?? 'unknown';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php` around
lines 114 - 117, In SummitAttendeeAuditLogFormatter, remove the extra blank
lines after the $removed_ids = $collectionData['removed_ids'] ?? []; assignment
so the assignment sits directly adjacent to the next statement; locate the
$removed_ids variable usage in this method (and the surrounding $collectionData
reference) and collapse the stray whitespace to match the project's whitespace
style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Audit/AuditLogOtlpStrategy.php`:
- Around line 190-203: The current conditional uses
!empty($change_set['deleted_ids']) which treats an explicitly-provided empty
deleted_ids as "missing" and falls back to counting $collection; change the
check to detect presence of the key (e.g., array_key_exists('deleted_ids',
$change_set) or isset($change_set['deleted_ids'])) so that an explicit empty
array is handled as a delete input; keep the existing logic that sets
$data['audit.collection_count'] = count($change_set['deleted_ids']) and the
subsequent collection-initialized branch (and calls to getCollectionChanges)
when the key exists, and only fall back to counting $collection when deleted_ids
is truly absent.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`:
- Around line 94-107: Rename the test method
testManyToManyDeleteReturnsNullWithoutRemovedIds to reflect that a non-null
formatted message is expected when removed IDs are empty (for example
testManyToManyDeleteFormatsEmptyRemovedIds); update the method name in the
SummitAttendeeAuditLogFormatterManyToManyTest class and any references so it
matches the assertion behavior where $formatter->format($attendee,
['collection'=>$collection]) is asserted with assertNotNull($result) and
assertStringContainsString(self::LOG_REMOVED_IDS_EMPTY, $result); keep the
existing calls to makeFormatter, makeCollection and the assertions unchanged.

---

Nitpick comments:
In `@app/Audit/AbstractAuditLogFormatter.php`:
- Around line 102-104: In AbstractAuditLogFormatter (the method containing the
app()->runningInConsole() check) there is an indentation mismatch: the line with
"return 'Worker Job';" is over-indented; adjust the indentation to match the
surrounding block style so the return aligns with the if block's opening brace
and other statements in the method.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php`:
- Around line 114-117: In SummitAttendeeAuditLogFormatter, remove the extra
blank lines after the $removed_ids = $collectionData['removed_ids'] ?? [];
assignment so the assignment sits directly adjacent to the next statement;
locate the $removed_ids variable usage in this method (and the surrounding
$collectionData reference) and collapse the stray whitespace to match the
project's whitespace style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad3fdd4c-48a1-4f00-9523-0d70be51d46d

📥 Commits

Reviewing files that changed from the base of the PR and between 1e069fd and 0d6d6aa.

📒 Files selected for processing (17)
  • app/Audit/AbstractAuditLogFormatter.php
  • app/Audit/AuditEventListener.php
  • app/Audit/AuditLogFormatterFactory.php
  • app/Audit/AuditLogOtlpStrategy.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/PresentationCategoryGroupAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php
  • app/Audit/PersistentCollectionMetadata.php
  • config/audit_log.php
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
  • tests/SummitRSVPServiceTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php

Comment on lines +190 to +203
if (!empty($change_set['deleted_ids'])) {
$data['audit.collection_count'] = count($change_set['deleted_ids']);
if ($collection->isInitialized()) {
$changes = $this->getCollectionChanges($collection, $change_set);
$data['audit.collection_current_count'] = $changes['current_count'];
$data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
$data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
} else {
$data['audit.collection_current_count'] = 0;
$data['audit.collection_snapshot_count'] = 0;
$data['audit.collection_is_dirty'] = 'true';
}
} elseif ($collection->isInitialized()) {
$data['audit.collection_count'] = count($collection);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Treat deleted_ids as explicit input even when empty.

Using !empty(...) conflates “missing” with “present but empty”. For delete events, that can produce inaccurate audit.collection_count by falling back to count($collection).

🐛 Proposed fix
-                    if (!empty($change_set['deleted_ids'])) {
-                        $data['audit.collection_count'] = count($change_set['deleted_ids']);
+                    if (array_key_exists('deleted_ids', $change_set) && is_array($change_set['deleted_ids'])) {
+                        $data['audit.collection_count'] = count($change_set['deleted_ids']);
                         if ($collection->isInitialized()) {
                             $changes = $this->getCollectionChanges($collection, $change_set);
                             $data['audit.collection_current_count'] = $changes['current_count'];
                             $data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
                             $data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
                         } else {
                             $data['audit.collection_current_count'] = 0;
                             $data['audit.collection_snapshot_count'] = 0;
                             $data['audit.collection_is_dirty'] = 'true';
                         }
                     } elseif ($collection->isInitialized()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditLogOtlpStrategy.php` around lines 190 - 203, The current
conditional uses !empty($change_set['deleted_ids']) which treats an
explicitly-provided empty deleted_ids as "missing" and falls back to counting
$collection; change the check to detect presence of the key (e.g.,
array_key_exists('deleted_ids', $change_set) or
isset($change_set['deleted_ids'])) so that an explicit empty array is handled as
a delete input; keep the existing logic that sets
$data['audit.collection_count'] = count($change_set['deleted_ids']) and the
subsequent collection-initialized branch (and calls to getCollectionChanges)
when the key exists, and only fall back to counting $collection when deleted_ids
is truly absent.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from 0d6d6aa to 2311779 Compare March 17, 2026 19:07
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php (1)

71-79: ⚠️ Potential issue | 🟡 Minor

“Without context” rows still don’t hit the no-context branch.

On Lines 71-79, all provider cases pass [], so provider rows on Lines 206-207 effectively test missing collection (same as Lines 208-209) instead of “collection present but context missing”.

🧪 Proposed test fix
     public function testManyToManyReturnsNullWithoutRequiredContextOrCollection(
         string $eventType,
-        bool $withContext
+        bool $withContext,
+        bool $withCollection
     ): void {
         $formatter = $this->makeFormatter($eventType, $withContext);
         $attendee = $this->makeAttendee();
-
-        $result = $formatter->format($attendee, []);
+        $changeSet = $withCollection
+            ? ['collection' => $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_EMPTY)]
+            : [];
+        $result = $formatter->format($attendee, $changeSet);
         $this->assertNull($result);
     }
@@
-            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false],
-            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false],
-            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true],
-            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true],
+            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false, true],
+            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false, true],
+            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true, false],
+            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true, false],
         ];
     }

Also applies to: 203-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`
around lines 71 - 79, The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection is passing an empty
array for the formatter context for all provider cases, so it never exercises
the "collection present but context missing" branch; update the data provider
used by makeFormatter to supply two separate cases: one where the second
argument is an empty array (both collection and context missing) and one where a
collection key (matching what SummitAttendeeAuditLogFormatter expects) is
present but the required context is omitted (e.g., pass ['collection' =>
$someCollection] or the formatter's actual collection key) so the
formatter->format($attendee, $context) call tests the "collection present but
context missing" path as well; adjust the provider rows that feed
testManyToManyReturnsNullWithoutRequiredContextOrCollection and any other
affected tests (e.g., the provider used by the test at lines noted) to include
these distinct cases.
app/Audit/AuditLogOtlpStrategy.php (1)

190-203: ⚠️ Potential issue | 🟡 Minor

Handle explicitly provided empty deleted_ids distinctly from “missing”.

On Line 190, !empty($change_set['deleted_ids']) treats [] as absent and falls back to counting the collection, which can misreport delete audit counts. Use key-existence + array validation instead.

🐛 Proposed fix
-                    if (!empty($change_set['deleted_ids'])) {
+                    if (array_key_exists('deleted_ids', $change_set) && is_array($change_set['deleted_ids'])) {
                         $data['audit.collection_count'] = count($change_set['deleted_ids']);
                         if ($collection->isInitialized()) {
                             $changes = $this->getCollectionChanges($collection, $change_set);
                             $data['audit.collection_current_count'] = $changes['current_count'];
                             $data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
                             $data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
                         } else {
                             $data['audit.collection_current_count'] = 0;
                             $data['audit.collection_snapshot_count'] = 0;
                             $data['audit.collection_is_dirty'] = 'true';
                         }
                     } elseif ($collection->isInitialized()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditLogOtlpStrategy.php` around lines 190 - 203, Replace the
!empty($change_set['deleted_ids']) check so an explicitly provided empty array
is treated as "present" instead of missing: use array_key_exists('deleted_ids',
$change_set) && is_array($change_set['deleted_ids']) to detect the key, then set
$data['audit.collection_count'] = count($change_set['deleted_ids']) (which will
be 0 for an empty array) and keep the subsequent logic that calls
getCollectionChanges($collection, $change_set) when
$collection->isInitialized(), otherwise set the current/snapshot counts and
is_dirty as before; update the condition that falls back to counting the
collection to only run when the deleted_ids key is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php`:
- Around line 64-72: The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection currently always
passes an empty change set to $formatter->format($group, []), so cases meant to
exercise "collection present but context missing" never run; change the call to
pass a change set conditionally: when $withContext is false provide a change set
that includes a collection key (e.g. ['collection' => /* use existing factory
like $this->makeCollection() or a minimal array representing the collection */])
so the branch where collection exists but context is missing is exercised, and
make the analogous change in the other test block referenced (the similar case
around lines 147-154 / the corresponding test method) to mirror this conditional
change-set behavior.

---

Duplicate comments:
In `@app/Audit/AuditLogOtlpStrategy.php`:
- Around line 190-203: Replace the !empty($change_set['deleted_ids']) check so
an explicitly provided empty array is treated as "present" instead of missing:
use array_key_exists('deleted_ids', $change_set) &&
is_array($change_set['deleted_ids']) to detect the key, then set
$data['audit.collection_count'] = count($change_set['deleted_ids']) (which will
be 0 for an empty array) and keep the subsequent logic that calls
getCollectionChanges($collection, $change_set) when
$collection->isInitialized(), otherwise set the current/snapshot counts and
is_dirty as before; update the condition that falls back to counting the
collection to only run when the deleted_ids key is absent.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`:
- Around line 71-79: The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection is passing an empty
array for the formatter context for all provider cases, so it never exercises
the "collection present but context missing" branch; update the data provider
used by makeFormatter to supply two separate cases: one where the second
argument is an empty array (both collection and context missing) and one where a
collection key (matching what SummitAttendeeAuditLogFormatter expects) is
present but the required context is omitted (e.g., pass ['collection' =>
$someCollection] or the formatter's actual collection key) so the
formatter->format($attendee, $context) call tests the "collection present but
context missing" path as well; adjust the provider rows that feed
testManyToManyReturnsNullWithoutRequiredContextOrCollection and any other
affected tests (e.g., the provider used by the test at lines noted) to include
these distinct cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff8d1c86-5523-4f14-8075-928c2e2533b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0d6d6aa and 2311779.

📒 Files selected for processing (5)
  • app/Audit/AuditEventListener.php
  • app/Audit/AuditLogOtlpStrategy.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
  • tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php

Comment on lines +64 to +72
public function testManyToManyReturnsNullWithoutRequiredContextOrCollection(
string $eventType,
bool $withContext
): void {
$formatter = $this->makeFormatter($eventType, $withContext);
$group = $this->makeGroup();

$result = $formatter->format($group, []);
$this->assertNull($result);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Provider currently duplicates missing-collection behavior for “without context” rows.

Line 71 always passes an empty change set, so the “without context” cases never validate the branch where collection is present but context is missing.

🧪 Proposed test fix
     public function testManyToManyReturnsNullWithoutRequiredContextOrCollection(
         string $eventType,
-        bool $withContext
+        bool $withContext,
+        bool $withCollection
     ): void {
         $formatter = $this->makeFormatter($eventType, $withContext);
         $group = $this->makeGroup();
-
-        $result = $formatter->format($group, []);
+        $changeSet = $withCollection
+            ? ['collection' => $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_EMPTY)]
+            : [];
+        $result = $formatter->format($group, $changeSet);
         $this->assertNull($result);
     }
@@
-            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false],
-            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false],
-            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true],
-            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true],
+            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false, true],
+            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false, true],
+            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true, false],
+            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true, false],
         ];
     }

Also applies to: 147-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php`
around lines 64 - 72, The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection currently always
passes an empty change set to $formatter->format($group, []), so cases meant to
exercise "collection present but context missing" never run; change the call to
pass a change set conditionally: when $withContext is false provide a change set
that includes a collection key (e.g. ['collection' => /* use existing factory
like $this->makeCollection() or a minimal array representing the collection */])
so the branch where collection exists but context is missing is exercised, and
make the analogous change in the other test block referenced (the similar case
around lines 147-154 / the corresponding test method) to mirror this conditional
change-set behavior.

Copy link
Copy Markdown
Contributor

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM codewise though there are merge conflicts currently.


if ($eventType === IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE
&& (
!$subject->isInitialized()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrestejerina97
The condition on lines 181-182 has two branches:

  1. Branch A (!isInitialized()) : collection was never loaded into memory. This is the primary scenario from
    https://github.com/OpenStackweb/summit-api/blob/b3d03599b463f7255ba20dc26dca1ef2326ef4d1/adr/0002-m2m-audit-logging-lightweight-join-table-query.md and the production incident in commit
    728ae67.
  2. Branch B (isInitialized() && getDeleteDiff() === 0) : collection was loaded but clear() took a new snapshot, so diffs are empty. This is the scenario from your comment here https://app.clickup.com/t/86b84w9x2?comment=90140187621105

testAuditCollectionDeleteInitializedWithoutDiffUsesJoinTableQuery covers Branch B (it calls takeSnapshot(), making the collection initialized). But Branch A , the ADR's primary scenario, has no test.

This matters because Branch A's !isInitialized() short-circuit is what prevents getDeleteDiff() from being called on an uninitialized collection, which would trigger Doctrine hydration and the memory blowup the ADR was written to prevent. A future refactor that removes the isInitialized() guard (e.g. simplifying to just count($subject->getDeleteDiff()) === 0) would reintroduce the production issue with no test to catch it.

Can you add a test similar to testAuditCollectionDeleteInitializedWithoutDiffUsesJoinTableQuery but without the $collection->takeSnapshot() call, so isInitialized() returns false? That would cover the uninitialized path end-to-end.

/**
* Build detailed message for many-to-many collection changes
*/
protected function buildManyToManyDetailedMessage(PersistentCollection $collection, array $insertDiff, array $deleteDiff): array
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrestejerina97
This method is never called , not by any formatter in this PR, and not anywhere else in the codebase (verified across all files on the branch).

All four formatters use the handleManyToManyCollection() → processCollection() pipeline instead, which produces the same ['field', 'target_entity', 'added_ids', 'removed_ids'] output shape.
Beyond being dead code, this method doesn't support preloadedDeletedIds (the join table query result from the ADR). If a future formatter used buildManyToManyDetailedMessage instead of processCollection for a DELETE event on an uninitialized collection, it would silently produce an empty audit log , the exact gap the ADR was written to close.
Suggest removing this method. The DTO pipeline (handleManyToManyCollection → processCollection) is the correct API and already covers all cases.

* @param string $eventType The event type constant (EVENT_COLLECTION_MANYTOMANY_DELETE or EVENT_COLLECTION_MANYTOMANY_UPDATE)
* @return array [$subject, $payload, $eventType]
*/
private function auditCollection($subject, $uow, string $eventType): array
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrestejerina97
$uow is passed here but never used inside the method body. Consider removing it from the signature and from both call sites (lines 59 and 67).

Copy link
Copy Markdown
Collaborator

@smarcet smarcet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
app/Audit/AuditEventListener.php (1)

115-136: ⚠️ Potential issue | 🟡 Minor

uiApp and uiFlow are permanently null in this path.

$ui is initialized to an empty array and never populated, so Lines 135-136 cannot emit any UI context. If dropping those fields was intentional, pass null directly and remove the dead variable; otherwise restore the request-derived extraction here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditEventListener.php` around lines 115 - 136, The uiApp/uiFlow
fields are always null because $ui is initialized as an empty array and never
populated; either remove $ui and pass nulls directly to AuditContext or restore
extraction from the Request before returning. Update the code in
AuditEventListener (around the request handling and return new
AuditContext(...)) to pull UI info from the request (e.g., decode a JSON
payload/header/session value into $ui and set uiApp/uiFlow) or replace uiApp:
$ui['app'] ?? null and uiFlow: $ui['flow'] ?? null with explicit nulls and
remove the dead $ui variable and any unused extraction logic. Ensure you
reference the AuditContext constructor and the uiApp/uiFlow parameters when
making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Audit/AuditEventListener.php`:
- Around line 160-168: AuditLogFormatterFactory currently assumes
PersistentCollection::getMapping() returns an array (using
$mapping['targetEntity']) which is inconsistent with other code paths; change
the usage in AuditLogFormatterFactory (where it reads mapping values) to handle
both object and array shapes like AuditEventListener::fetchManyToManyIds():
obtain $mapping via getMapping(), then read values defensively using object
property access ($mapping->targetEntity, $mapping->fieldName) falling back to
array access ($mapping['targetEntity'], $mapping['fieldName']) if properties are
not present, and update any related logic in AuditLogFormatterFactory to use
these normalized values.

---

Duplicate comments:
In `@app/Audit/AuditEventListener.php`:
- Around line 115-136: The uiApp/uiFlow fields are always null because $ui is
initialized as an empty array and never populated; either remove $ui and pass
nulls directly to AuditContext or restore extraction from the Request before
returning. Update the code in AuditEventListener (around the request handling
and return new AuditContext(...)) to pull UI info from the request (e.g., decode
a JSON payload/header/session value into $ui and set uiApp/uiFlow) or replace
uiApp: $ui['app'] ?? null and uiFlow: $ui['flow'] ?? null with explicit nulls
and remove the dead $ui variable and any unused extraction logic. Ensure you
reference the AuditContext constructor and the uiApp/uiFlow parameters when
making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6db7ab85-e6e9-49d9-bc87-c8d6b7a92dd7

📥 Commits

Reviewing files that changed from the base of the PR and between 2311779 and ff3b041.

📒 Files selected for processing (3)
  • app/Audit/AbstractAuditLogFormatter.php
  • app/Audit/AuditEventListener.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/Audit/AbstractAuditLogFormatter.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from ff3b041 to 14e01c5 Compare April 6, 2026 17:52
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

📘 OpenAPI / Swagger preview

➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-498/

This page is automatically updated on each push to this PR.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
app/Audit/AuditLogOtlpStrategy.php (1)

193-206: ⚠️ Potential issue | 🟡 Minor

Treat deleted_ids as explicit input even when it's empty.

fetchManyToManyIds() can legitimately return []. Using !empty(...) collapses that into “missing” and falls back to count($collection), which can skew audit.collection_count for empty-delete cases.

🐛 Proposed fix
-                    if (!empty($change_set['deleted_ids'])) {
+                    if (array_key_exists('deleted_ids', $change_set) && is_array($change_set['deleted_ids'])) {
                         $data['audit.collection_count'] = count($change_set['deleted_ids']);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditLogOtlpStrategy.php` around lines 193 - 206, The code
currently uses !empty($change_set['deleted_ids']) which treats an explicit empty
array as "missing" and wrongly falls back to count($collection); change the
condition to check for the presence of the key (e.g.
array_key_exists('deleted_ids', $change_set) or
isset($change_set['deleted_ids'])) so that an explicit [] from
fetchManyToManyIds() is honored, and when the key exists set
audit.collection_count = count($change_set['deleted_ids']) and keep the existing
logic that computes current/snapshot/is_dirty via
getCollectionChanges($collection, $change_set) or the else branch when the
collection is not initialized.
tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php (1)

63-72: ⚠️ Potential issue | 🟡 Minor

The “without context” rows still never pass a collection.

testManyToManyReturnsNullWithoutRequiredContextOrCollection() always calls format($group, []), so the provider rows named “without context” still only exercise the missing-collection path. Add a withCollection flag and pass a real collection for those rows.

🧪 Proposed fix
     public function testManyToManyReturnsNullWithoutRequiredContextOrCollection(
         string $eventType,
-        bool $withContext
+        bool $withContext,
+        bool $withCollection
     ): void {
         $formatter = $this->makeFormatter($eventType, $withContext);
         $group = $this->makeGroup();
-
-        $result = $formatter->format($group, []);
+        $changeSet = $withCollection
+            ? ['collection' => $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_EMPTY)]
+            : [];
+        $result = $formatter->format($group, $changeSet);
         $this->assertNull($result);
     }
@@
-            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false],
-            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false],
-            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true],
-            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true],
+            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false, true],
+            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false, true],
+            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true, false],
+            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true, false],
         ];
     }

Also applies to: 147-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php`
around lines 63 - 72, The test always passes an empty array as the collection
context, so rows meant to exercise the “without context” case never provide a
real collection; update the data provider providesNullCasesForManyToMany to
return a third boolean flag (e.g., withCollection) and change
testManyToManyReturnsNullWithoutRequiredContextOrCollection signature to accept
bool $withCollection, then when $withCollection is true pass a real collection
(e.g., $this->makeCollection()) to $formatter->format($group, $collection) and
otherwise pass [] as before; apply the same change to the other similar
test/provider pair around lines 147-154 so both tests correctly exercise
missing-context vs missing-collection paths.
app/Audit/AuditEventListener.php (1)

115-136: ⚠️ Potential issue | 🟡 Minor

uiApp and uiFlow are still guaranteed to be null.

$ui is initialized as [] and never populated, so Lines 135-136 always emit null. Either hydrate these values from request context or stop threading them through AuditContext here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditEventListener.php` around lines 115 - 136, The $ui array is
initialized but never populated so AuditContext fields uiApp and uiFlow (set at
uiApp and uiFlow) will always be null; fix by extracting the UI payload from the
current request before returning AuditContext (e.g. read
$req->attributes->get('ui') or $req->get('ui') or decode a known header like
'X-UI' and set $ui['app'] and $ui['flow'] accordingly), then pass those
populated values into the AuditContext constructor, or if no UI is available
remove uiApp/uiFlow from AuditContext here; update the code around
request()/$req and AuditContext usage to use the actual source you choose.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php`:
- Around line 96-109: The test
testFormatWithDeletedIdsEmptyPayloadReturnsFallbackMessage currently uses
SNAPSHOT_IDS_EMPTY and CURRENT_IDS_REMOVE_ONE which yields an already-empty
derived delete diff, so it doesn't validate that an explicit deleted_ids: []
takes precedence; update the collection creation to use a pair that produces an
actual removal (e.g. replace the makeCollection arguments with a snapshot that
contains the item and a current set that omits it such as
self::SNAPSHOT_IDS_REMOVE_ONE and self::CURRENT_IDS_EMPTY or any equivalent
constants in the test suite) so the derived diff would be non-empty and the
assertion will fail if explicit DELETED_IDS_EMPTY_PAYLOAD is ignored; keep the
rest of the test (formatter->format call and assertions) unchanged.

---

Duplicate comments:
In `@app/Audit/AuditEventListener.php`:
- Around line 115-136: The $ui array is initialized but never populated so
AuditContext fields uiApp and uiFlow (set at uiApp and uiFlow) will always be
null; fix by extracting the UI payload from the current request before returning
AuditContext (e.g. read $req->attributes->get('ui') or $req->get('ui') or decode
a known header like 'X-UI' and set $ui['app'] and $ui['flow'] accordingly), then
pass those populated values into the AuditContext constructor, or if no UI is
available remove uiApp/uiFlow from AuditContext here; update the code around
request()/$req and AuditContext usage to use the actual source you choose.

In `@app/Audit/AuditLogOtlpStrategy.php`:
- Around line 193-206: The code currently uses
!empty($change_set['deleted_ids']) which treats an explicit empty array as
"missing" and wrongly falls back to count($collection); change the condition to
check for the presence of the key (e.g. array_key_exists('deleted_ids',
$change_set) or isset($change_set['deleted_ids'])) so that an explicit [] from
fetchManyToManyIds() is honored, and when the key exists set
audit.collection_count = count($change_set['deleted_ids']) and keep the existing
logic that computes current/snapshot/is_dirty via
getCollectionChanges($collection, $change_set) or the else branch when the
collection is not initialized.

In
`@tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php`:
- Around line 63-72: The test always passes an empty array as the collection
context, so rows meant to exercise the “without context” case never provide a
real collection; update the data provider providesNullCasesForManyToMany to
return a third boolean flag (e.g., withCollection) and change
testManyToManyReturnsNullWithoutRequiredContextOrCollection signature to accept
bool $withCollection, then when $withCollection is true pass a real collection
(e.g., $this->makeCollection()) to $formatter->format($group, $collection) and
otherwise pass [] as before; apply the same change to the other similar
test/provider pair around lines 147-154 so both tests correctly exercise
missing-context vs missing-collection paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46387668-fc22-40da-a542-723c2bb515c5

📥 Commits

Reviewing files that changed from the base of the PR and between ff3b041 and 14e01c5.

📒 Files selected for processing (18)
  • app/Audit/AbstractAuditLogFormatter.php
  • app/Audit/AuditEventListener.php
  • app/Audit/AuditLogFormatterFactory.php
  • app/Audit/AuditLogOtlpStrategy.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/PresentationCategoryGroupAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php
  • app/Audit/Interfaces/IAuditStrategy.php
  • app/Audit/PersistentCollectionMetadata.php
  • config/audit_log.php
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
✅ Files skipped from review due to trivial changes (7)
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
  • app/Audit/PersistentCollectionMetadata.php
  • app/Audit/Interfaces/IAuditStrategy.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php
  • config/audit_log.php
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • app/Audit/AuditLogFormatterFactory.php
  • app/Audit/ConcreteFormatters/PresentationCategoryGroupAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php
  • app/Audit/AbstractAuditLogFormatter.php

Comment on lines +96 to +109
public function testFormatWithDeletedIdsEmptyPayloadReturnsFallbackMessage(): void
{
$this->formatter->setContext(AuditContextBuilder::default()->build());

$collection = $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_REMOVE_ONE);
$result = $this->formatter->format(new \stdClass(), [
'collection' => $collection,
'deleted_ids' => self::DELETED_IDS_EMPTY_PAYLOAD,
]);

$this->assertNotNull($result);
$this->assertStringContainsString(self::LOG_DELETED_PREFIX, $result);
$this->assertStringContainsString(self::LOG_REMOVED_IDS_EMPTY, $result);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This empty-payload case doesn't prove payload precedence.

With SNAPSHOT_IDS_EMPTYCURRENT_IDS_REMOVE_ONE, the derived delete diff is already empty, so missing deleted_ids and explicit deleted_ids: [] both still produce Removed IDs: []. Use a collection with an actual removal here so this test fails if explicit empty payloads are ignored.

🧪 Proposed fix
-        $collection = $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_REMOVE_ONE);
+        $collection = $this->makeCollection(self::SNAPSHOT_IDS_REMOVE_ONE, self::CURRENT_IDS_REMOVE_ONE);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function testFormatWithDeletedIdsEmptyPayloadReturnsFallbackMessage(): void
{
$this->formatter->setContext(AuditContextBuilder::default()->build());
$collection = $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_REMOVE_ONE);
$result = $this->formatter->format(new \stdClass(), [
'collection' => $collection,
'deleted_ids' => self::DELETED_IDS_EMPTY_PAYLOAD,
]);
$this->assertNotNull($result);
$this->assertStringContainsString(self::LOG_DELETED_PREFIX, $result);
$this->assertStringContainsString(self::LOG_REMOVED_IDS_EMPTY, $result);
}
public function testFormatWithDeletedIdsEmptyPayloadReturnsFallbackMessage(): void
{
$this->formatter->setContext(AuditContextBuilder::default()->build());
$collection = $this->makeCollection(self::SNAPSHOT_IDS_REMOVE_ONE, self::CURRENT_IDS_REMOVE_ONE);
$result = $this->formatter->format(new \stdClass(), [
'collection' => $collection,
'deleted_ids' => self::DELETED_IDS_EMPTY_PAYLOAD,
]);
$this->assertNotNull($result);
$this->assertStringContainsString(self::LOG_DELETED_PREFIX, $result);
$this->assertStringContainsString(self::LOG_REMOVED_IDS_EMPTY, $result);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php`
around lines 96 - 109, The test
testFormatWithDeletedIdsEmptyPayloadReturnsFallbackMessage currently uses
SNAPSHOT_IDS_EMPTY and CURRENT_IDS_REMOVE_ONE which yields an already-empty
derived delete diff, so it doesn't validate that an explicit deleted_ids: []
takes precedence; update the collection creation to use a pair that produces an
actual removal (e.g. replace the makeCollection arguments with a snapshot that
contains the item and a current set that omits it such as
self::SNAPSHOT_IDS_REMOVE_ONE and self::CURRENT_IDS_EMPTY or any equivalent
constants in the test suite) so the derived diff would be non-empty and the
assertion will fail if explicit DELETED_IDS_EMPTY_PAYLOAD is ignored; keep the
rest of the test (formatter->format call and assertions) unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants